using Aragorn.SOA.ORM.Lambda;
using Microsoft.CSharp.RuntimeBinder;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using Aragorn.SOA.ORM.Lambda.Builder;
using Aragorn.SOA.ORM.Lambda.Resolver.ExpressionTree;
using Aragorn.SOA.ORM.Lambda.ValueObjects;
using Aragorn.SOA.ORM.Lambda.Builder;
using Aragorn.SOA.ORM.Lambda.Resolver.ExpressionTree;

namespace Aragorn.SOA.ORM.Lambda.Resolver
{
	internal class LambdaResolver
	{
		private Dictionary<ExpressionType, string> _operationDictionary = new Dictionary<ExpressionType, string>()
		{
			{ ExpressionType.Equal, "=" },
			{ ExpressionType.NotEqual, "!=" },
			{ ExpressionType.GreaterThan, ">" },
			{ ExpressionType.LessThan, "<" },
			{ ExpressionType.GreaterThanOrEqual, ">=" },
			{ ExpressionType.LessThanOrEqual, "<=" }
		};

		private SqlQueryBuilder _builder
		{
			get;
			set;
		}

		public LambdaResolver(SqlQueryBuilder builder)
		{
			this._builder = builder;
		}

		private void BuildSql(Node node)
		{
			this.BuildSql((dynamic)node);
		}

		private void BuildSql(LikeNode node)
		{
			if (node.Method == LikeMethod.Equals)
			{
				this._builder.QueryByField(node.MemberNode.TableName, node.MemberNode.FieldName, this._operationDictionary[ExpressionType.Equal], node.Value);
				return;
			}
			string value = node.Value;
			switch (node.Method)
			{
				case LikeMethod.StartsWith:
				{
					value = string.Concat(node.Value, "%");
					break;
				}
				case LikeMethod.EndsWith:
				{
					value = string.Concat("%", node.Value);
					break;
				}
				case LikeMethod.Contains:
				{
					value = string.Concat("%", node.Value, "%");
					break;
				}
			}
			this._builder.QueryByFieldLike(node.MemberNode.TableName, node.MemberNode.FieldName, value);
		}

		private void BuildSql(OperationNode node)
		{
			this.BuildSql((dynamic)node.Left, (dynamic)node.Right, node.Operator);
		}

		private void BuildSql(MemberNode memberNode)
		{
			this._builder.QueryByField(memberNode.TableName, memberNode.FieldName, this._operationDictionary[ExpressionType.Equal], true);
		}

		private void BuildSql(SingleOperationNode node)
		{
			if (node.Operator == ExpressionType.Not)
			{
				this._builder.Not();
			}
			this.BuildSql(node.Child);
		}

		private void BuildSql(MemberNode memberNode, ValueNode valueNode, ExpressionType op)
		{
			if (valueNode.Value == null)
			{
				this.ResolveNullValue(memberNode, op);
				return;
			}
			this._builder.QueryByField(memberNode.TableName, memberNode.FieldName, this._operationDictionary[op], valueNode.Value);
		}

		private void BuildSql(ValueNode valueNode, MemberNode memberNode, ExpressionType op)
		{
			this.BuildSql(memberNode, valueNode, op);
		}

		private void BuildSql(MemberNode leftMember, MemberNode rightMember, ExpressionType op)
		{
			this._builder.QueryByFieldComparison(leftMember.TableName, leftMember.FieldName, this._operationDictionary[op], rightMember.TableName, rightMember.FieldName);
		}

		private void BuildSql(SingleOperationNode leftMember, Node rightMember, ExpressionType op)
		{
			if (leftMember.Operator == ExpressionType.Not)
			{
				this.BuildSql((Node)leftMember, rightMember, op);
				return;
			}
			this.BuildSql((dynamic)leftMember.Child, (dynamic)rightMember, op);
		}

		private void BuildSql(Node leftMember, SingleOperationNode rightMember, ExpressionType op)
		{
			this.BuildSql(rightMember, leftMember, op);
		}

		private void BuildSql(Node leftNode, Node rightNode, ExpressionType op)
		{
			this._builder.BeginExpression();
			this.BuildSql((dynamic)leftNode);
			this.ResolveOperation(op);
			this.BuildSql((dynamic)rightNode);
			this._builder.EndExpression();
		}

		private static BinaryExpression GetBinaryExpression(Expression expression)
		{
			if (!(expression is BinaryExpression))
			{
				throw new ArgumentException("Binary expression expected");
			}
			return expression as BinaryExpression;
		}

		public static string GetColumnName<T>(Expression<Func<T, object>> selector)
		{
			return LambdaResolver.GetColumnName(LambdaResolver.GetMemberExpression(selector.Body));
		}

		public static string GetColumnName(Expression expression)
		{
			MemberExpression memberExpression = LambdaResolver.GetMemberExpression(expression);
			SqlLamColumnAttribute sqlLamColumnAttribute = memberExpression.Member.GetCustomAttributes(false).OfType<SqlLamColumnAttribute>().FirstOrDefault<SqlLamColumnAttribute>();
			if (sqlLamColumnAttribute != null)
			{
				return sqlLamColumnAttribute.Name;
			}
			return memberExpression.Member.Name;
		}

		private object GetExpressionValue(Expression expression)
		{
			ExpressionType nodeType = expression.NodeType;
			if (nodeType == ExpressionType.Call)
			{
				return this.ResolveMethodCall(expression as MethodCallExpression);
			}
			if (nodeType == ExpressionType.Constant)
			{
				return (expression as ConstantExpression).Value;
			}
			if (nodeType != ExpressionType.MemberAccess)
			{
				throw new ArgumentException("Expected constant expression");
			}
			MemberExpression memberExpression = expression as MemberExpression;
			object expressionValue = this.GetExpressionValue(memberExpression.Expression);
			return this.ResolveValue((dynamic)memberExpression.Member, expressionValue);
		}

		private static MemberExpression GetMemberExpression(Expression expression)
		{
			ExpressionType nodeType = expression.NodeType;
			if (nodeType == ExpressionType.Convert)
			{
				return LambdaResolver.GetMemberExpression((expression as UnaryExpression).Operand);
			}
			if (nodeType != ExpressionType.MemberAccess)
			{
				throw new ArgumentException("Member expression expected");
			}
			return expression as MemberExpression;
		}

		public static string GetTableName<T>()
		{
			return LambdaResolver.GetTableName(typeof(T));
		}

		public static string GetTableName(Type type)
		{
			SqlLamTableAttribute sqlLamTableAttribute = type.GetCustomAttributes(false).OfType<SqlLamTableAttribute>().FirstOrDefault<SqlLamTableAttribute>();
			if (sqlLamTableAttribute != null)
			{
				return sqlLamTableAttribute.Name;
			}
			return type.Name;
		}

		private static string GetTableName(MemberExpression expression)
		{
			return LambdaResolver.GetTableName(expression.Member.DeclaringType);
		}

		public void GroupBy<T>(Expression<Func<T, object>> expression)
		{
			this.GroupBy<T>(LambdaResolver.GetMemberExpression(expression.Body));
		}

		private void GroupBy<T>(MemberExpression expression)
		{
			string columnName = LambdaResolver.GetColumnName(LambdaResolver.GetMemberExpression(expression));
			this._builder.GroupBy(LambdaResolver.GetTableName<T>(), columnName);
		}

		public void Join<T1, T2>(Expression<Func<T1, T2, bool>> expression)
		{
			BinaryExpression binaryExpression = LambdaResolver.GetBinaryExpression(expression.Body);
			MemberExpression memberExpression = LambdaResolver.GetMemberExpression(binaryExpression.Left);
			this.Join<T1, T2>(memberExpression, LambdaResolver.GetMemberExpression(binaryExpression.Right));
		}

		public void Join<T1, T2, TKey>(Expression<Func<T1, TKey>> leftExpression, Expression<Func<T1, TKey>> rightExpression)
		{
			this.Join<T1, T2>(LambdaResolver.GetMemberExpression(leftExpression.Body), LambdaResolver.GetMemberExpression(rightExpression.Body));
		}

		public void Join<T1, T2>(MemberExpression leftExpression, MemberExpression rightExpression)
		{
			this._builder.Join(LambdaResolver.GetTableName<T1>(), LambdaResolver.GetTableName<T2>(), LambdaResolver.GetColumnName(leftExpression), LambdaResolver.GetColumnName(rightExpression));
		}

		public void OrderBy<T>(Expression<Func<T, object>> expression, bool desc = false)
		{
			string columnName = LambdaResolver.GetColumnName(LambdaResolver.GetMemberExpression(expression.Body));
			this._builder.OrderBy(LambdaResolver.GetTableName<T>(), columnName, desc);
		}

		public void QueryByIsIn<T>(Expression<Func<T, object>> expression, SqlLamBase sqlQuery)
		{
			string columnName = LambdaResolver.GetColumnName<T>(expression);
			this._builder.QueryByIsIn(LambdaResolver.GetTableName<T>(), columnName, sqlQuery);
		}

		public void QueryByIsIn<T>(Expression<Func<T, object>> expression, IEnumerable<object> values)
		{
			string columnName = LambdaResolver.GetColumnName<T>(expression);
			this._builder.QueryByIsIn(LambdaResolver.GetTableName<T>(), columnName, values);
		}

		public void QueryByNotIn<T>(Expression<Func<T, object>> expression, SqlLamBase sqlQuery)
		{
			string columnName = LambdaResolver.GetColumnName<T>(expression);
			this._builder.Not();
			this._builder.QueryByIsIn(LambdaResolver.GetTableName<T>(), columnName, sqlQuery);
		}

		public void QueryByNotIn<T>(Expression<Func<T, object>> expression, IEnumerable<object> values)
		{
			string columnName = LambdaResolver.GetColumnName<T>(expression);
			this._builder.Not();
			this._builder.QueryByIsIn(LambdaResolver.GetTableName<T>(), columnName, values);
		}

		private object ResolveMethodCall(MethodCallExpression callExpression)
		{
			object obj;
			object[] array = callExpression.Arguments.Select<Expression, object>(new Func<Expression, object>(this.GetExpressionValue)).ToArray<object>();
			obj = (callExpression.Object != null ? this.GetExpressionValue(callExpression.Object) : array.First<object>());
			return callExpression.Method.Invoke(obj, array);
		}

		private void ResolveNullValue(MemberNode memberNode, ExpressionType op)
		{
			ExpressionType expressionType = op;
			if (expressionType == ExpressionType.Equal)
			{
				this._builder.QueryByFieldNull(memberNode.TableName, memberNode.FieldName);
				return;
			}
			if (expressionType != ExpressionType.NotEqual)
			{
				return;
			}
			this._builder.QueryByFieldNotNull(memberNode.TableName, memberNode.FieldName);
		}

		private void ResolveOperation(ExpressionType op)
		{
			ExpressionType expressionType = op;
			switch (expressionType)
			{
				case ExpressionType.And:
				case ExpressionType.AndAlso:
				{
					this._builder.And();
					return;
				}
				default:
				{
					switch (expressionType)
					{
						case ExpressionType.Or:
						case ExpressionType.OrElse:
						{
							this._builder.Or();
							return;
						}
					}
					break;
				}
			}
			throw new ArgumentException(string.Format("Unrecognized binary expression operation '{0}'", op.ToString()));
		}

		public void ResolveQuery<T>(Expression<Func<T, bool>> expression)
		{
			this.BuildSql(this.ResolveQuery((dynamic)expression.Body));
		}




        private Node ResolveQuery(ConstantExpression constantExpression)
		{
			return new ValueNode()
			{
				Value = constantExpression.Value
			};
		}

		private Node ResolveQuery(UnaryExpression unaryExpression)
		{
		    return new SingleOperationNode
		    {
		        Operator = unaryExpression.NodeType,
		        Child = (Node) this.ResolveQuery((dynamic) unaryExpression.Operand)
		    };
		}



        private Node ResolveQuery(BinaryExpression binaryExpression)
        {
            return new OperationNode { Left = (Node)this.ResolveQuery((dynamic)binaryExpression.Left), Operator = binaryExpression.NodeType, Right = (Node)this.ResolveQuery((dynamic)binaryExpression.Right) };
        }


        private Node ResolveQuery(MethodCallExpression callExpression)
		{
			LikeMethod likeMethod;
			if (!Enum.TryParse<LikeMethod>(callExpression.Method.Name, true, out likeMethod))
			{
				return new ValueNode()
				{
					Value = this.ResolveMethodCall(callExpression)
				};
			}
			MemberExpression obj = callExpression.Object as MemberExpression;
			string expressionValue = (string)this.GetExpressionValue(callExpression.Arguments.First<Expression>());
			LikeNode likeNode = new LikeNode();
			MemberNode memberNode = new MemberNode()
			{
				TableName = LambdaResolver.GetTableName(obj),
				FieldName = LambdaResolver.GetColumnName(callExpression.Object)
			};
			likeNode.MemberNode = memberNode;
			likeNode.Method = likeMethod;
			likeNode.Value = expressionValue;
			return likeNode;
		}

		private Node ResolveQuery(MemberExpression memberExpression, MemberExpression rootExpression = null)
		{
			rootExpression = rootExpression ?? memberExpression;
			ExpressionType nodeType = memberExpression.Expression.NodeType;
			if (nodeType > ExpressionType.Constant)
			{
				if (nodeType == ExpressionType.MemberAccess)
				{
					return this.ResolveQuery(memberExpression.Expression as MemberExpression, rootExpression);
				}
				if (nodeType == ExpressionType.Parameter)
				{
					MemberNode memberNode = new MemberNode()
					{
						TableName = LambdaResolver.GetTableName(rootExpression),
						FieldName = LambdaResolver.GetColumnName(rootExpression)
					};
					return memberNode;
				}
			}
			else if (nodeType == ExpressionType.Call || nodeType == ExpressionType.Constant)
			{
				return new ValueNode()
				{
					Value = this.GetExpressionValue(rootExpression)
				};
			}
			throw new ArgumentException("Expected member expression");
		}




        private void ResolveQuery(Expression expression)
        {
            throw new ArgumentException(string.Format("The provided expression '{0}' is currently not supported", expression.NodeType));
        }

        private void ResolveSingleOperation(ExpressionType op)
		{
			if (op != ExpressionType.Not)
			{
				return;
			}
			this._builder.Not();
		}

		private object ResolveValue(PropertyInfo property, object obj)
		{
			return property.GetValue(obj, null);
		}

		private object ResolveValue(FieldInfo field, object obj)
		{
			return field.GetValue(obj);
		}

		public void Select<T>(Expression<Func<T, object>> expression)
		{
			this.Select<T>(expression.Body);
		}

		private void Select<T>(Expression expression)
		{
			ExpressionType nodeType = expression.NodeType;
			if (nodeType > ExpressionType.MemberAccess)
			{
				if (nodeType == ExpressionType.New)
				{
					foreach (MemberExpression argument in (expression as NewExpression).Arguments)
					{
						this.Select<T>(argument);
					}
					return;
				}
				if (nodeType == ExpressionType.Parameter)
				{
					this._builder.Select(LambdaResolver.GetTableName(expression.Type));
					return;
				}
			}
			else if (nodeType == ExpressionType.Convert || nodeType == ExpressionType.MemberAccess)
			{
				this.Select<T>(LambdaResolver.GetMemberExpression(expression));
				return;
			}
			throw new ArgumentException("Invalid expression");
		}

		private void Select<T>(MemberExpression expression)
		{
			if (expression.Type.IsClass && expression.Type != typeof(string))
			{
				this._builder.Select(LambdaResolver.GetTableName(expression.Type));
				return;
			}
			this._builder.Select(LambdaResolver.GetTableName<T>(), LambdaResolver.GetColumnName(expression));
		}

		public void SelectWithFunction<T>(Expression<Func<T, object>> expression, SelectFunction selectFunction)
		{
			this.SelectWithFunction<T>(expression.Body, selectFunction);
		}

		private void SelectWithFunction<T>(Expression expression, SelectFunction selectFunction)
		{
			string columnName = LambdaResolver.GetColumnName(LambdaResolver.GetMemberExpression(expression));
			this._builder.Select(LambdaResolver.GetTableName<T>(), columnName, selectFunction);
		}
	}
}